package com.ccx.myinterpreter;

import com.ccx.myinterpreter.Expr.Assignment;
import com.ccx.myinterpreter.Expr.Binary;
import com.ccx.myinterpreter.Expr.Call;
import com.ccx.myinterpreter.Expr.Get;
import com.ccx.myinterpreter.Expr.Grouping;
import com.ccx.myinterpreter.Expr.Literal;
import com.ccx.myinterpreter.Expr.Set;
import com.ccx.myinterpreter.Expr.Super;
import com.ccx.myinterpreter.Expr.Unary;
import com.ccx.myinterpreter.Expr.Variable;
import com.ccx.myinterpreter.Stmt.Block;
import com.ccx.myinterpreter.Stmt.ClassDecl;
import com.ccx.myinterpreter.Stmt.Expression;
import com.ccx.myinterpreter.Stmt.FunDecl;
import com.ccx.myinterpreter.Stmt.IfStmt;
import com.ccx.myinterpreter.Stmt.Print;
import com.ccx.myinterpreter.Stmt.ReturnStmt;
import com.ccx.myinterpreter.Stmt.VarDecl;
import com.ccx.myinterpreter.Stmt.WhileStmt;
import com.ccx.myinterpreter.Stmt.forStmt;

import static com.ccx.myinterpreter.TokenType.*;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Interpreter implements Expr.Visitor, Stmt.Visitor {

    Envirement globalEnv;
    Envirement currentEnv;
    private Map<Expr, Integer> varBindMap = new HashMap<>();

    public Interpreter() {
        globalEnv = new Envirement(null);
        currentEnv = globalEnv;

        globalEnv.declaration("clock", new Callable() {

            @Override
            public Object call(Interpreter interpreter, List<Object> args) {
                return Double.valueOf(System.currentTimeMillis());
            }

            @Override
            public int arity() {
                return 0;
            }
            
        });
    }

    public void interpreter(List<Stmt> stmts) {
        for (Stmt stmt : stmts) {
            stmt.accept(this);
        }
    }

    private static void pipe(String src, Interpreter interpreter) {
        // System.out.println("expr:");
        // System.out.println(src);
        // System.out.println();

        Scanner scanner = new Scanner(src);
        scanner.scan();
        // System.out.println("tokens:");
        // for(Token token : scanner.list) {
        //     System.out.println(token);
        // }
        // System.out.println();

        Parser parser = new Parser(scanner.list);
        List<Stmt> stmts = parser.parse();
        // System.out.println("statements:");
        // for(Stmt stmt : stmts) {
        //     System.out.println(stmt.getClass());
        // }
        // System.out.println();

        // System.out.println("result:");
        Resolver resolver = new Resolver(interpreter);
        resolver.resolve(stmts);
        interpreter.interpreter(stmts);
    }

    private static void repl() {
        java.util.Scanner sc = new java.util.Scanner(System.in);
        Interpreter interpreter = new Interpreter();
        while (true) {
            System.out.print("> ");
            String line = sc.nextLine();
            pipe(line, interpreter);
        }
    }

    public static void main(String[] args) throws Exception {
        if(args.length == 0) {
            repl();
        } else if(args.length == 1) {
            byte[] srcBytes = Files.readAllBytes(Paths.get(args[0]));
            String src = new String(srcBytes, StandardCharsets.UTF_8);
            Interpreter interpreter = new Interpreter();
            pipe(src, interpreter);
        } else {
            System.out.println("参数错误");
        }
    }

    public static void test(String[] args) {
        // String src = "print 1 + 2 * (3 - 4) + 6 / 2;";
        // String src = "print \"hello world!\";";
        // String src = "print true;";
        // String src = "var a = 100 * 20 + 20 / 2; if(1 + 1 == 2 or a < 100){a = a + 100; print \"a\"; print a;} else {var b = 200; print \"b\";print b;}";
        // String src = "var a = 10; while(a > 0) {print a; a = a - 1;}";
        // String src = "for(var a = 10; a > 0; a = a - 1) {print a;}";
        // String src = "fun fab(n){if(n < 2) return n;return n * fab(n - 1);} print fab(5);";
        // String src = "fun create_fun(n) {fun inner_fun() {print n;} return inner_fun;} var myfun = create_fun(10); myfun();";
        // String src = "var a = 11; { fun showA() { a = a+1; print a;} showA(); var a = 22; showA(); }";
        String src = "class Obj {name(){print \"name: \" + this.n;}} "
        + "class Box < Obj { init(w, h){this.w=w;this.h=h;} width(){ print \"width: \" + this.w; } height(){print \"height:\" + this.h;} info(){super.name();this.width();this.height();} } "
        + "var box = Box(100, 200); box.n = \"mybox\"; box.name(); box.width(); box.height(); box.info();";
        System.out.println("expr:");
        System.out.println(src);
        System.out.println();

        Scanner scanner = new Scanner(src);
        scanner.scan();
        System.out.println("tokens:");
        for(Token token : scanner.list) {
            System.out.println(token);
        }
        System.out.println();

        Parser parser = new Parser(scanner.list);
        List<Stmt> stmts = parser.parse();
        System.out.println("statements:");
        for(Stmt stmt : stmts) {
            System.out.println(stmt.getClass());
        }
        System.out.println();

        System.out.println("result:");
        Interpreter interpreter = new Interpreter();
        Resolver resolver = new Resolver(interpreter);
        resolver.resolve(stmts);
        interpreter.interpreter(stmts);
    }

    /**
     * 将变量绑定到环境
     * @param expr 变量,即Variable表达式
     * @param instance 距离最内层环境的距离,0表示最内层
     */
    void varBind(Expr variable, Integer instance) {
        varBindMap.put(variable, instance);
    }

    boolean isEqual(Object a, Object b) {
        if(a == null && b == null) return true;
        if(a == null) return false;
        return a.equals(b);
    }

    boolean isTrue(Object obj) {
        if(obj == null) return false;
        if(obj instanceof Boolean) return (boolean) obj;
        return true;
    }

    void executeBlock(Block block, Envirement env) {
        Envirement prev = currentEnv;
        this.currentEnv = env;
        try {
            for(Stmt stmt : block.stmts) {
                stmt.accept(this);
            }
        } finally {
            this.currentEnv = prev;
        }
    }

    @Override
    public Object visitLiteral(Literal literal) {
        return literal.value;
    }

    @Override
    public Object visitSuper(Super superExpr) {
        int distance = varBindMap.get(superExpr);
        LoxClass superClass = (LoxClass) currentEnv.getAt("super", distance);
        LoxInstance instance = (LoxInstance) currentEnv.getAt("this", distance - 1);
        return superClass.findMethod(superExpr.name.getLexeme()).bindThis(instance);
    }

    @Override
    public Object visitVariable(Variable variable) {
        Integer distance = varBindMap.get(variable);
        if(distance != null)
            return currentEnv.getAt(variable.name.getLexeme(), distance);
        else
            return globalEnv.get(variable.name.getLexeme());
    }

    @Override
    public Object visitGrouping(Grouping grouping) {
        return grouping.expr.accept(this);
    }

    @Override
    public Object visitUnary(Unary unary) {
        Object val = unary.right.accept(this);
        if(unary.operator.getType() == MINUS) {
            if(val instanceof Double) {
                return -((double)val);
            } else {
                throw new RuntimeException("unexpected operand " + val + " for " + unary.operator.getLexeme());
            }
        } else if(unary.operator.getType() == BANG) {
            if(val instanceof Boolean) {
                return !((boolean)val);
            } else {
                throw new RuntimeException("unexpected operand " + val + " for " + unary.operator.getLexeme());
            }
        }
        throw new RuntimeException("unknown operator " + unary.operator.getLexeme());
    }

    @Override
    public Object visitCall(Call call) {
        Object calleeObj = call.callee.accept(this);
        if(!(calleeObj instanceof Callable)) {
            throw new RuntimeException("expect callable callee when call!");
        }
        Callable callee = (Callable) calleeObj;
        if(call.args.size() != callee.arity())
            throw new RuntimeException("expect " + callee.arity() + " args but got " + call.args.size() + "!");
        List<Object> args = new ArrayList<>();
        for(Expr arg : call.args) {
            args.add(arg.accept(this));
        }
        return callee.call(this, args);
    }

    @Override
    public Object visitGet(Get get) {
        Object obj = get.object.accept(this);
        if(!(obj instanceof LoxInstance))
            throw new RuntimeException("expect instance before .!");
        LoxInstance instance = (LoxInstance) obj;
        return instance.get(get.name.getLexeme());
    }

    @Override
    public Object visitSet(Set set) {
        Object obj = set.object.accept(this);
        if(!(obj instanceof LoxInstance))
            throw new RuntimeException("expect instance before .!");
        LoxInstance instance = (LoxInstance) obj;
        Object val = set.value.accept(this);
        instance.getFields().put(set.name.getLexeme(), val);
        return val;
    }

    @Override
    public Object visitBinary(Binary binary) {
        Object left = binary.left.accept(this);
        Object right = binary.right.accept(this);
        if(binary.operator.getType() == STAR) {
            if(left instanceof Double && right instanceof Double) {
                return ((double) left) * ((double) right);
            } else {
                throw new RuntimeException("unexpected operand " + left + "," + right + " for " + binary.operator.getLexeme());
            }
        } else if(binary.operator.getType() == SLASH) {
            if(left instanceof Double && right instanceof Double) {
                return ((double) left) / ((double) right);
            } else {
                throw new RuntimeException("unexpected operand " + left + "," + right + " for " + binary.operator.getLexeme());
            }
        } else if(binary.operator.getType() == PLUS) {
            if(left instanceof Double && right instanceof Double) {
                return ((double) left) + ((double) right);
            } else {
                return  "" + left + right;
            }
        } else if(binary.operator.getType() == MINUS) {
            if(left instanceof Double && right instanceof Double) {
                return ((double) left) - ((double) right);
            } else {
                throw new RuntimeException("unexpected operand " + left + "," + right + " for " + binary.operator.getLexeme());
            }
        } else if(binary.operator.getType() == GREATER) {
            if(left instanceof Double && right instanceof Double) {
                return ((double) left) > ((double) right);
            } else {
                throw new RuntimeException("unexpected operand " + left + "," + right + "f or " + binary.operator.getLexeme());
            }
        } else if(binary.operator.getType() == GREATER_EQUAL) {
            if(left instanceof Double && right instanceof Double) {
                return ((double) left) >= ((double) right);
            } else {
                throw new RuntimeException("unexpected operand " + left + "," + right + "for " + binary.operator.getLexeme());
            }
        } else if(binary.operator.getType() == LESS) {
            if(left instanceof Double && right instanceof Double) {
                return ((double) left) < ((double) right);
            } else {
                throw new RuntimeException("unexpected operand " + left + "," + right + "for " + binary.operator.getLexeme());
            }
        } else if(binary.operator.getType() == LESS_EQUAL) {
            if(left instanceof Double && right instanceof Double) {
                return ((double) left) <= ((double) right);
            } else {
                throw new RuntimeException("unexpected operand " + left + "," + right + "for " + binary.operator.getLexeme());
            }
        } else if(binary.operator.getType() == EQUAL_EQUAL) {
            return isEqual(left, right);
        } else if(binary.operator.getType() == BANG_EQUAL) {
            return !isEqual(left, right);
        } else if(binary.operator.getType() == AND) {
            return isTrue(left) && isTrue(right);
        } else if(binary.operator.getType() == OR) {
            return isTrue(left) || isTrue(right);
        }
        throw new RuntimeException("unknown operator " + binary.operator.getLexeme());
    }

    @Override
    public Object visitAssignment(Assignment assignment) {
        Object val = assignment.expr.accept(this);
        Integer instance = varBindMap.get(assignment);
        if(instance != null)
            currentEnv.setAt(assignment.name.getLexeme(), val, instance);
        else
            globalEnv.set(assignment.name.getLexeme(), val);
        return val;
    }

    @Override
    public void visitExpr(Expression stmt) {
        stmt.expr.accept(this);
    }

    @Override
    public void visitPrint(Print stmt) {
        Object val = stmt.expr.accept(this);
        if(val instanceof Double) {
            val = String.format("%.2f", val);
        }
        System.out.println(val);
    }

    @Override
    public void visitVarDecl(VarDecl stmt) {
        Object val = null;
        if(stmt.expr != null) {
            val = stmt.expr.accept(this);
        }
        currentEnv.declaration(stmt.identifier.getLexeme(), val);
    }

    @Override
    public void visitBlock(Block block) {
        Envirement env = new Envirement(currentEnv);
        executeBlock(block, env);
    }

    @Override
    public void visitIf(IfStmt ifStmt) {
        Object condition = ifStmt.expr.accept(this);
        if(isTrue(condition)) {
            ifStmt.thenStmt.accept(this);
        } else if(ifStmt.elseStmt != null) {
            ifStmt.elseStmt.accept(this);
        }
    }

    @Override
    public void visitWhileStmt(WhileStmt whileStmt) {
        while (isTrue(whileStmt.condiation.accept(this))) {
            whileStmt.stmt.accept(this);
        }
    }

    @Override
    public void visitForStmt(forStmt forStmt) {
        if(forStmt.first != null) forStmt.first.accept(this);
        while (forStmt.second == null ? true : isTrue(forStmt.second.accept(this))) {
            forStmt.body.accept(this);
            if(forStmt.third != null)
                forStmt.third.accept(this);
        }
    }

    @Override
    public void visitFunDecl(FunDecl funDecl) {
        LoxFun loxFun = new LoxFun(funDecl, currentEnv);
        currentEnv.declaration(funDecl.name.getLexeme(), loxFun);
    }

    @Override
    public void visitReturnStmt(ReturnStmt returnStmt) {
        Object val = null;
        if(returnStmt.expr != null) {
            val = returnStmt.expr.accept(this);
        }
        throw new ReturnException(val);
    }

    @Override
    public void visitClassDecl(ClassDecl classDecl) {
        LoxClass superClass = null;
        if(classDecl.superClass != null) {
            superClass = (LoxClass) classDecl.superClass.accept(this);
            currentEnv = new Envirement(currentEnv);
            currentEnv.declaration("super", superClass);
        }
        Map<String, LoxFun> methods = new HashMap<>();
        for(Stmt.FunDecl funDecl : classDecl.methods) {
            LoxFun loxFun = null;
            String methodName = funDecl.name.getLexeme();
            if(methodName.equals("init")) {
                loxFun = new LoxFun(funDecl, currentEnv, true);
            } else {
                loxFun = new LoxFun(funDecl, currentEnv);
            }
            methods.put(methodName, loxFun);
        }
        if(classDecl.superClass != null) {
            currentEnv = currentEnv.getParent();
        }
        
        LoxClass loxClass = new LoxClass(classDecl.name.getLexeme(), methods, superClass);
        currentEnv.declaration(classDecl.name.getLexeme(), loxClass);
    }

}
